import contextlib
import functools
import mimetypes
from datetime import datetime, timedelta
from io import BytesIO
from uuid import UUID, uuid4

from asgiref.sync import sync_to_async
from csp.constants import NONE
from csp.decorators import csp
from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction
from django.db.models import (
    CharField,
    F,
    ImageField,
    Prefetch,
    ProtectedError,
    Q,
    Value,
    prefetch_related_objects,
)
from django.db.models.functions import Cast
from django.forms import model_to_dict
from django.http import Http404
from django.shortcuts import get_object_or_404
from django.urls.converters import StringConverter, UUIDConverter
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_control
from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import OpenApiParameter, OpenApiTypes, extend_schema
from rest_framework import exceptions, mixins, status, views, viewsets
from rest_framework.decorators import action
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.pagination import LimitOffsetPagination
from rest_framework.response import Response
from rest_framework.serializers import ValidationError
from rest_framework.settings import api_settings

from sysreptor.pentests.fielddefinition.predefined_fields import FINDING_FIELDS_PREDEFINED
from sysreptor.pentests.filtersets import (
    ArchivedProjectFilterSet,
    FindingTemplateFilter,
    FindingTemplateOrderingFilter,
    FindingTemplateSearchFilter,
    PentestProjectFilterSet,
    ProjectTypeFilter,
    ProjectTypeOrderingFilter,
)
from sysreptor.pentests.import_export import (
    export_notes,
    export_project_types,
    export_projects,
    export_templates,
    import_notes,
    import_project_types,
    import_projects,
    import_templates,
)
from sysreptor.pentests.models import (
    ArchivedProject,
    ArchivedProjectKeyPart,
    Comment,
    CommentAnswer,
    FindingTemplate,
    FindingTemplateTranslation,
    LockStatus,
    PentestFinding,
    PentestProject,
    ProjectMemberInfo,
    ProjectMemberRole,
    ProjectNotebookExcalidrawFile,
    ProjectNotebookPage,
    ProjectType,
    ProjectTypeScope,
    ReportSection,
    UploadedAsset,
    UploadedImage,
    UploadedProjectFile,
    UploadedTemplateImage,
    UserPublicKey,
)
from sysreptor.pentests.models.notes import ShareInfo
from sysreptor.pentests.permissions import (
    ArchivedProjectKeyPartPermissions,
    CommentPermissions,
    IsTemplateEditorOrReadOnly,
    ProjectPermissions,
    ProjectSubresourcePermissions,
    ProjectTypePermissions,
    ProjectTypeSubresourcePermissions,
    SharedProjectNotePublicPermissions,
    ShareInfoPermissions,
    ShareInfoPublicPermissions,
    UserPublicKeyPermissions,
)
from sysreptor.pentests.rendering.entry import (
    render_note_to_pdf,
    render_pdf,
    render_pdf_preview,
    render_project_markdown_fields_to_html,
)
from sysreptor.pentests.serializers.archive import (
    ArchivedProjectKeyPartDecryptSerializer,
    ArchivedProjectKeyPartSerializer,
    ArchivedProjectPublicKeyEncryptedKeyPartSerializer,
    ArchivedProjectSerializer,
    PentestProjectCheckArchiveSerializer,
    PentestProjectCreateArchiveSerializer,
    UserPublicKeyRegisterBeginSerializer,
    UserPublicKeySerializer,
)
from sysreptor.pentests.serializers.common import (
    CopySerializer,
    ErrorMessageSerializer,
    ExportSerializer,
    HistoryTimelineSerializer,
    ImportSerializer,
    LockableObjectSerializer,
    PdfResponseSerializer,
    TagsSerializer,
)
from sysreptor.pentests.serializers.files import (
    UploadedAssetSerializer,
    UploadedImageSerializer,
    UploadedProjectFileSerilaizer,
    UploadedTemplateImageSerializer,
    UploadedUserNotebookFileSerilaizer,
    UploadedUserNotebookImageSerializer,
)
from sysreptor.pentests.serializers.notes import (
    ExportNotesOptionsSerializer,
    ExportPdfMultipleOptionsSerializer,
    ExportPdfOptionsSerializer,
    NoteExcalidrawDataSerializer,
    ProjectNotebookPageCreatePublicSerializer,
    ProjectNotebookPageCreateSerializer,
    ProjectNotebookPagePublicSerializer,
    ProjectNotebookPageSerializer,
    ProjectNotebookPageSortListSerializer,
    ShareInfoCheckPasswordSerializer,
    ShareInfoPublicSerializer,
    ShareInfoSerializer,
    UserNotebookPageCreateSerializer,
    UserNotebookPageSerializer,
    UserNotebookPageSortListSerializer,
)
from sysreptor.pentests.serializers.project import (
    CommentAnswerSerializer,
    CommentCreateSerializer,
    CommentResolveSerializer,
    CommentSerializer,
    CustomizeProjectTypeSerializer,
    Md2HtmlOptionsSerializer,
    PentestFindingFromTemplateSerializer,
    PentestFindingSerializer,
    PentestFindingSortListSerializer,
    PentestProjectCheckSerializer,
    PentestProjectCopySerializer,
    PentestProjectDetailSerializer,
    PentestProjectReadonlySerializer,
    PentestProjectShortSerializer,
    PreviewPdfOptionsSerializer,
    PublishPdfOptionsSerializer,
    ReportSectionSerializer,
)
from sysreptor.pentests.serializers.project_type import (
    ProjectTypeCopySerializer,
    ProjectTypeCreateSerializer,
    ProjectTypeDetailSerializer,
    ProjectTypeImportSerializer,
    ProjectTypePreviewSerializer,
    ProjectTypeShortSerializer,
)
from sysreptor.pentests.serializers.template import (
    FindingTemplateFromPentestFindingSerializer,
    FindingTemplateSerializer,
    FindingTemplateShortSerializer,
    FindingTemplateTranslationSerializer,
)
from sysreptor.users.permissions import UserNotebookPermissions
from sysreptor.users.views import APIBadRequestError, UserSubresourceViewSetMixin
from sysreptor.utils import license
from sysreptor.utils.api import (
    CursorMultiPagination,
    FileResponseAsync,
    StreamingHttpResponseAsync,
    ViewSetAsync,
)
from sysreptor.utils.fielddefinition.types import serialize_field_definition
from sysreptor.utils.utils import parse_date_string


class ViewSetMixinHelper:
    def get_serializer_for_action(self, action, **kwargs):
        action_bak = self.action
        try:
            self.action = action
            return self.get_serializer(**kwargs)
        finally:
            self.action = action_bak


class LockableViewSetMixin(ViewSetMixinHelper):
    def get_serializer_class(self):
        if self.action in ['lock', 'unlock']:
            return LockableObjectSerializer
        return super().get_serializer_class()

    @action(detail=True, methods=['post'])
    def lock(self, request, *args, **kwargs):
        instance = self.get_object()

        lock_status = instance.lock(request.user, refresh_lock=request.data.get('refresh_lock', True))
        instance.refresh_from_db()
        serializer = self.get_serializer_for_action('get', instance=instance)
        return Response(serializer.data, status={
            LockStatus.CREATED: status.HTTP_201_CREATED,
            LockStatus.REFRESHED: status.HTTP_200_OK,
            LockStatus.FAILED: status.HTTP_403_FORBIDDEN,
        }[lock_status])

    @action(detail=True, methods=['post'])
    def unlock(self, request, *args, **kwargs):
        instance = self.get_object()
        if not instance.unlock(request.user):
            raise exceptions.PermissionDenied('Could not lock object')

        serializer = self.get_serializer_for_action('get', instance=instance)
        return Response(serializer.data)

    @contextlib.contextmanager
    def _ensure_locked(self, instance):
        was_locked = instance.is_locked
        if instance.lock(self.request.user, refresh_lock=False) == LockStatus.FAILED:
            raise exceptions.PermissionDenied('Could not lock object')
        yield instance
        if not was_locked and instance.pk is not None:
            instance.unlock(self.request.user)

    def perform_update(self, serializer):
        with self._ensure_locked(serializer.instance):
            return super().perform_update(serializer)

    def perform_destroy(self, instance):
        with self._ensure_locked(instance):
            return super().perform_destroy(instance)


class ExportImportViewSetMixin(ViewSetMixinHelper):
    def get_serializer_class(self):
        if self.action == 'export':
            return ExportSerializer
        elif self.action == 'import_':
            return ImportSerializer
        else:
            return super().get_serializer_class()

    @extend_schema(responses={(200, 'application/octet-stream'): OpenApiTypes.BINARY})
    @action(detail=True, methods=['post'])
    def export(self, request, **kwargs):
        instance = self.get_object()
        archive = self.perform_export([instance])
        return StreamingHttpResponseAsync(streaming_content=archive, headers={
            'Content-Type': 'application/octet-stream',
            'Content-Disposition': 'inline',
        })

    def perform_export(self, instances):
        pass

    @action(detail=False, url_path='import', url_name='import', methods=['post'])
    def import_(self, request, **kwargs):
        import_serializer = self.get_serializer(data=request.data)
        import_serializer.is_valid(raise_exception=True)

        with import_serializer.validated_data['file'].open('rb') as f:
            imported_instances = self.perform_import(f, data=import_serializer.validated_data)
        result_serializer = self.get_serializer_for_action('get', instance=imported_instances, many=True)
        return Response(result_serializer.data, status=status.HTTP_201_CREATED)

    def perform_import(self, archive, data, **kwargs):
        pass


class CopyViewSetMixin:
    def get_serializer_class(self):
        if self.action == 'copy':
            return CopySerializer
        return super().get_serializer_class()

    @action(detail=True, methods=['post'])
    def copy(self, request, *args, **kwargs):
        instance = self.get_object()
        request_serializer = self.get_serializer(instance=instance, data=request.data)
        request_serializer.is_valid(raise_exception=True)
        instance_cp = request_serializer.save()

        response_serializer = self.get_serializer_for_action('get', instance=instance_cp)
        return Response(response_serializer.data, status=status.HTTP_201_CREATED)


class TagViewSetMixin:
    def get_serializer_class(self):
        if self.action == 'tags':
            return TagsSerializer
        return super().get_serializer_class()

    @action(detail=False, methods=['get'])
    def tags(self, request, *args, **kwargs):
        tags = self.get_queryset().get_all_tags()
        serializer = self.get_serializer(instance={'tags': tags})
        return Response(serializer.data)


class HistoryTimelineViewSetMixin:
    def get_serializer_class(self):
        if self.action == 'history_timeline':
            return HistoryTimelineSerializer
        return super().get_serializer_class()

    @action(detail=True, methods=['get'], url_path='history-timeline', pagination_class=LimitOffsetPagination)
    def history_timeline(self, request, *args, **kwargs):
        if not license.is_professional():
            raise license.LicenseError('Professional license required')

        # Django does not support filtering querysets after union().
        # This prevents us from using the default cursor pagination, because this breaks history timelines from multiple tables.
        # Instead we have to use LimitOffsetPagination here.
        queryset = self.get_history_timeline_queryset()
        page = self.paginate_queryset(queryset)
        serializer = self.get_serializer(instance=page, many=True)
        return self.get_paginated_response(serializer.data)

    def get_history_timeline_queryset(self):
        timeline_querysets = self.get_history_timeline_queryset_parts()
        queryset = None
        for qs in timeline_querysets:
            if 'model_id' not in qs.query.annotations:
                qs = qs.annotate(model_id=Cast(F('id'), output_field=CharField()))
            if 'history_model_order' not in qs.query.annotations:
                qs = qs.annotate(history_model_order=Value(qs.model.instance_type.__name__))

            # Hide auto-save entries before first cleanup
            qs = qs.exclude(Q(history_change_reason=None) & Q(history_prevent_cleanup=False) & Q(history_date__gt=timezone.now() - timedelta(minutes=6)))
            # Prepare for union
            qs = qs.annotate(history_model=Value(qs.model.instance_type.__name__)) \
                .select_related('history_user') \
                .only('history_date', 'history_type', 'history_user', 'history_title', 'history_change_reason', 'id')
            if queryset is None:
                queryset = qs
            else:
                queryset = queryset.union(qs)

        return queryset \
            .order_by('-history_date', '-history_model_order', 'id')

    def get_history_timeline_queryset_parts(self):
        return []


@extend_schema(parameters=[OpenApiParameter(name='history_date', type=datetime, location=OpenApiParameter.PATH)])
class HistoryViewSetBase(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
    lookup_url_kwarg = 'history_date'
    lookup_value_regex = '[^/]+'

    def get_history_date(self):
        try:
            return parse_date_string(self.kwargs[self.lookup_url_kwarg])
        except ValueError as ex:
            raise ValidationError('Invalid history_date') from ex

    def get_queryset(self):
        return []

    def get_base_object(self):
        return None

    def get_object(self):
        instance = self.get_base_object()
        self.check_object_permissions(self.request, instance)
        return instance

    def retrieve_historic(self, get_instance=None, prefetch=None):
        try:
            instance = get_instance() if get_instance else self.get_object().history.as_of(self.get_history_date())
        except ObjectDoesNotExist:
            raise Http404() from None

        if prefetch:
                prefetch_related_objects([instance], *prefetch)
        serializer = self.get_serializer(instance=instance)
        return Response(data=serializer.data)

    def retrieve_historic_file(self, model, filename):
        queryset = model.history \
            .as_of(self.get_history_date()) \
            .filter(linked_object_id=self.get_object().id) \
            .filter(name_hash=model.hash_name(filename))
        instance = get_object_or_404(queryset)
        return file_response(instance)

    def retrieve(self, request, *args, **kwargs):
        return self.retrieve_historic()


@extend_schema(parameters=[OpenApiParameter(name='project_id', type=UUID, location=OpenApiParameter.PATH)])
@extend_schema(parameters=[OpenApiParameter(name='id', type=UUID, location=OpenApiParameter.PATH)])
class ProjectSubresourceMixin(views.APIView):
    permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES + [ProjectSubresourcePermissions]
    pagination_class = None

    @functools.cached_property
    def _get_project(self):
        if not self.request:
            return None
        qs = PentestProject.objects \
            .only_permitted(self.request.user) \
            .select_related('project_type')
        return get_object_or_404(qs, pk=self.kwargs.get('project_pk'))

    def get_project(self):
        return self._get_project

    def get_serializer_context(self):
        return super().get_serializer_context() | {
            'project': self.get_project(),
        }


@extend_schema(parameters=[OpenApiParameter(name='finding_id', type=UUID, location=OpenApiParameter.PATH)])
class PentestFindingSubresourceMixin(ProjectSubresourceMixin):
    @functools.cached_property
    def _get_finding(self):
        return get_object_or_404(self.get_project().findings, finding_id=self.kwargs.get('finding_id'))

    def get_finding(self):
        return self._get_finding

    def get_serializer_context(self):
        return super().get_serializer_context() | {
            'finding': self.get_finding(),
        }


@extend_schema(parameters=[OpenApiParameter(name='section_id', type=UUID, location=OpenApiParameter.PATH)])
class ReportSectionSubresourceMixin(ProjectSubresourceMixin):
    @functools.cached_property
    def _get_section(self):
        return get_object_or_404(self.get_project().sections, section_id=self.kwargs.get('section_id'))

    def get_section(self):
        return self._get_section

    def get_serializer_context(self):
        return super().get_serializer_context() | {
            'section': self.get_section(),
        }



@extend_schema(parameters=[OpenApiParameter(name='projecttype_id', type=UUID, location=OpenApiParameter.PATH)])
@extend_schema(parameters=[OpenApiParameter(name='id', type=UUID, location=OpenApiParameter.PATH)])
class ProjectTypeSubresourceMixin(views.APIView):
    permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES + [ProjectTypeSubresourcePermissions]

    @functools.cached_property
    def _get_project_type(self):
        qs = ProjectType.objects \
            .only_permitted(self.request.user) \
            .select_related('linked_project', 'linked_user') \
            .prefetch_related(Prefetch('linked_project__members', queryset=ProjectMemberInfo.objects.select_related('user')))
        return get_object_or_404(qs, pk=self.kwargs['projecttype_pk'])

    def get_project_type(self):
        return self._get_project_type

    def get_serializer_context(self):
        return super().get_serializer_context() | {
            'project_type': self.get_project_type(),
        }


@extend_schema(parameters=[OpenApiParameter(name='template_id', type=UUID, location=OpenApiParameter.PATH)])
@extend_schema(parameters=[OpenApiParameter(name='id', type=UUID, location=OpenApiParameter.PATH)])
class TemplateSubresourceMixin(views.APIView):
    permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES + [IsTemplateEditorOrReadOnly]
    pagination_class = None

    @functools.cached_property
    def _get_template(self):
        if not self.request:
            return None
        qs = FindingTemplate.objects \
            .select_related('lock_info_data', 'lock_info_data__user')
        return get_object_or_404(qs, pk=self.kwargs['template_pk'])

    def get_template(self):
        return self._get_template

    def get_serializer_context(self):
        return super().get_serializer_context() | {
            'template': self.get_template(),
        }

    def _ensure_locked(self):
        return LockableViewSetMixin._ensure_locked(self, self.get_template())

    def perform_create(self, serializer):
        with self._ensure_locked():
            return super().perform_create(serializer)

    def perform_update(self, serializer):
        with self._ensure_locked():
            return super().perform_update(serializer)

    def perform_destroy(self, instance):
        with self._ensure_locked():
            return super().perform_destroy(instance)


@extend_schema(parameters=[OpenApiParameter(name='id', type=UUID, location=OpenApiParameter.PATH)])
class ProjectTypeViewSet(LockableViewSetMixin, CopyViewSetMixin, TagViewSetMixin, ExportImportViewSetMixin, HistoryTimelineViewSetMixin, viewsets.ModelViewSet, ViewSetAsync):
    permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES + [ProjectTypePermissions]
    serializer_class = ProjectTypeDetailSerializer
    filter_backends = [SearchFilter, DjangoFilterBackend, ProjectTypeOrderingFilter]
    search_fields = ['name', 'tags']
    filterset_class = ProjectTypeFilter
    pagination_class = CursorMultiPagination

    def get_serializer_class(self):
        if self.action == 'list':
            return ProjectTypeShortSerializer
        elif self.action == 'create':
            return ProjectTypeCreateSerializer
        elif self.action == 'preview':
            return ProjectTypePreviewSerializer
        elif self.action == 'copy':
            return ProjectTypeCopySerializer
        elif self.action == 'import_':
            return ProjectTypeImportSerializer
        elif self.action == 'import_notes':
            return ImportSerializer
        return super().get_serializer_class()

    def get_queryset(self):
        qs = ProjectType.objects \
            .select_related('lock_info_data', 'lock_info_data__user', 'linked_project', 'linked_user') \
            .prefetch_related(Prefetch('linked_project__members', queryset=ProjectMemberInfo.objects.select_related('user'))) \
            .only_permitted(self.request.user)
        return qs

    @action(detail=False, url_path='predefinedfields/findings', methods=['get'])
    def get_predefined_finding_fields(self, request, *args, **kwargs):
        return Response(data=serialize_field_definition(FINDING_FIELDS_PREDEFINED))

    @action(detail=True, methods=['post'], throttle_scope='pdf')
    async def preview(self, request, *args, **kwargs):
        instance = await self.aget_object()
        serializer = await self.aget_valid_serializer(data=request.data)

        d = serializer.validated_data
        pdf_preview = await render_pdf_preview(
            report_template=d.get('report_template', ''),
            report_styles=d.get('report_styles', ''),
            report_preview_data=(d.get('report_preview_data') or {}) | {
                'pentesters': [ProjectMemberInfo(user=self.request.user, roles=await sync_to_async(lambda: ProjectMemberRole.default_roles)())],
            },
            project_type=instance,
        )
        return Response(data=pdf_preview.to_dict())

    @action(detail=True, url_path='import-notes', methods=['post'])
    def import_notes(self, request, *args, **kwargs):
        project_type = self.get_object()
        import_serializer = self.get_serializer(data=request.data)
        import_serializer.is_valid(raise_exception=True)

        with self._ensure_locked(project_type):
            with import_serializer.validated_data['file'].open('rb') as f:
                imported_instances = import_notes(f, context={'project_type': project_type})
            return Response(imported_instances, status=status.HTTP_200_OK)

    def perform_export(self, instances):
        return export_project_types(instances)

    def perform_import(self, archive, data, **kwargs):
        instances = import_project_types(archive, **kwargs)
        if data.get('scope') == ProjectTypeScope.PRIVATE:
            for i in instances:
                i.linked_user = self.request.user
            ProjectType.objects.bulk_update(instances, fields=['linked_user'])
            ProjectType.history.filter(id__in=map(lambda pt: pt.id, instances)).update(linked_user_id=self.request.user.id)
        return instances

    def get_history_timeline_queryset_parts(self):
        return [
            self.get_object().history.all(),
        ]


class ProjectTypeHistoryViewSet(ProjectTypeSubresourceMixin, HistoryViewSetBase):
    permission_classes = ProjectTypeSubresourceMixin.permission_classes + [license.ProfessionalLicenseRequired]
    serializer_class = ProjectTypeDetailSerializer

    def get_base_object(self):
        return self.get_project_type()

    @extend_schema(responses={(200, 'application/octet-stream'): OpenApiTypes.BINARY})
    @action(detail=True, url_path='assets/name/(?P<filename>[^/]+)')
    @method_decorator(cache_control(max_age=60 * 60 * 24, private=True))
    def asset_by_name(self, request, *arg, **kwargs):
        return self.retrieve_historic_file(UploadedAsset, filename=self.kwargs['filename'])


@extend_schema(parameters=[OpenApiParameter(name='id', type=UUID, location=OpenApiParameter.PATH)])
class PentestProjectViewSet(CopyViewSetMixin, TagViewSetMixin, ExportImportViewSetMixin, HistoryTimelineViewSetMixin, viewsets.ModelViewSet, ViewSetAsync):
    permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES + [ProjectPermissions]
    serializer_class = PentestProjectDetailSerializer
    filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
    search_fields = ['name', 'tags', 'language', 'members__user__username', 'members__user__email']
    filterset_fields = ['language', 'readonly']
    ordering_fields = ['created', 'updated', 'name']
    filterset_class = PentestProjectFilterSet

    def get_serializer_class(self):
        if self.action == 'list':
            return PentestProjectShortSerializer
        if self.action == 'generate':
            return PublishPdfOptionsSerializer
        elif self.action == 'preview':
            return PreviewPdfOptionsSerializer
        elif self.action == 'md2html':
            return Md2HtmlOptionsSerializer
        elif self.action == 'check':
            return PentestProjectCheckSerializer
        elif self.action == 'readonly':
            return PentestProjectReadonlySerializer
        elif self.action == 'copy':
            return PentestProjectCopySerializer
        elif self.action == 'upload_image_or_file':
            return UploadedProjectFileSerilaizer
        elif self.action == 'export_all':
            return ExportSerializer
        elif self.action == 'customize_projecttype':
            return CustomizeProjectTypeSerializer
        elif self.action == 'archive_check':
            return PentestProjectCheckArchiveSerializer
        elif self.action == 'archive':
            return PentestProjectCreateArchiveSerializer
        return super().get_serializer_class()

    def get_queryset(self):
        qs = PentestProject.objects \
            .only_permitted(self.request.user) \
            .prefetch_related(Prefetch('members', queryset=ProjectMemberInfo.objects.select_related('user')))
        if self.action != 'list':
            qs = qs.select_related('project_type')
        if self.action in ['check', 'preview', 'generate', 'retrieve']:
            qs = qs.prefetch_related(
                Prefetch('sections', ReportSection.objects.select_related('assignee').prefetch_related('comments')),
                Prefetch('findings', PentestFinding.objects.select_related('assignee').prefetch_related('comments')),
            )
        return qs

    @action(detail=True, methods=['get'])
    def check(self, request, *args, **kwargs):
        return self.partial_update(request, *args, **kwargs)

    @action(detail=True, methods=['get', 'patch', 'put'])
    def readonly(self, request, *args, **kwargs):
        if request.method == 'get':
            return self.retrieve(request, *args, **kwargs)
        else:
            return self.partial_update(request, *args, **kwargs)

    @action(detail=True, url_path='customize-projecttype', methods=['post'])
    def customize_projecttype(self, request, *args, **kwargs):
        return self.partial_update(request, *args, **kwargs)

    @action(detail=True, url_path='upload', methods=['post'])
    def upload_image_or_file(self, request, *args, **kwargs):
        # First try saving an image, then saving as a regular file
        serializer_context = self.get_serializer_context() | {'project': self.get_object()}
        serializer = UploadedImageSerializer(data=request.data, context=serializer_context)
        if not serializer.is_valid(raise_exception=False):
            serializer = UploadedProjectFileSerilaizer(data=request.data, context=serializer_context)
            serializer.is_valid(raise_exception=True)

        serializer.save()
        return Response(data=serializer.data, status=status.HTTP_201_CREATED)

    @extend_schema(responses={(200, 'application/octet-stream'): OpenApiTypes.BINARY})
    @action(detail=True, methods=['post'], url_path='export/all')
    def export_all(self, *args, **kwargs):
        return self.export(*args, **kwargs)

    def perform_export(self, instances):
        return export_projects(instances, export_all=self.action == 'export_all')

    def perform_import(self, archive, data, **kwargs):
        projects = import_projects(archive, **kwargs)
        PentestProject.objects.add_member(user=self.request.user, projects=projects)
        return projects

    @extend_schema(responses={201: ArchivedProjectSerializer})
    @action(detail=True, methods=['post'])
    def archive(self, request, *args, **kwargs):
        project = self.get_object()
        serializer = self.get_serializer(data=request.data, context=self.get_serializer_context() | {'project': project})
        serializer.is_valid(raise_exception=True)
        archive = serializer.save()

        archive_serializer = ArchivedProjectSerializer(instance=archive, context=self.get_serializer_context())
        return Response(data=archive_serializer.data, status=status.HTTP_201_CREATED)

    @action(detail=True, url_path='archive-check', methods=['get'])
    def archive_check(self, request, *args, **kwargs):
        return self.partial_update(request, *args, **kwargs)

    @extend_schema(responses=PdfResponseSerializer)
    @action(detail=True, methods=['post'], throttle_scope='pdf')
    async def preview(self, request, *args, **kwargs):
        instance = await self.aget_object()
        serializer = await self.aget_valid_serializer(instance, data=request.data)
        options = serializer.validated_data

        pdf_preview = await render_pdf(project=instance, **options)
        return Response(data=pdf_preview.to_dict())

    @extend_schema(responses={(200, 'application/pdf'): OpenApiTypes.BINARY, 400: PdfResponseSerializer})
    @action(detail=True, methods=['post'], throttle_scope='pdf')
    async def generate(self, request, *args, **kwargs):
        instance = await self.aget_object()
        serializer = await self.aget_valid_serializer(instance, data=request.data)
        options = serializer.validated_data

        # Generate final report; optionally encrypt PDF if a password was supplied
        data = await render_pdf(
            project=instance,
            password=options.get('password'),
            can_compress_pdf=True,
        )
        if data.pdf:
            return FileResponseAsync(BytesIO(data.pdf), content_type='application/pdf')
        else:
            return Response(data=data.to_dict(), status=status.HTTP_400_BAD_REQUEST)

    @extend_schema(responses={200: PentestProjectDetailSerializer, 400: ErrorMessageSerializer(many=True)})
    @action(detail=True, methods=['post'], throttle_scope='pdf')
    async def md2html(self, request, *args, **kwargs):
        instance = await self.aget_object()

        data = await render_project_markdown_fields_to_html(project=instance, request=request)
        if data.get('result'):
            return Response(data=data['result'])
        else:
            return Response(data=data['messages'], status=status.HTTP_400_BAD_REQUEST)

    def get_history_timeline_queryset_parts(self):
        p = self.get_object()

        sections_history = ReportSection.history \
            .filter(project_id=p.id) \
            .annotate(history_model_order=Value('02')) \
            .annotate(model_id=F('section_id'))
        findings_history = PentestFinding.history \
            .filter(project_id=p.id) \
            .annotate(history_model_order=Value('03')) \
            .annotate(model_id=Cast(F('finding_id'), output_field=CharField()))
        notes_history = ProjectNotebookPage.history \
            .filter(project_id=p.id) \
            .annotate(history_model_order=Value('04')) \
            .annotate(model_id=Cast(F('note_id'), output_field=CharField()))

        res = []
        history_mode = self.request.GET.get('mode', 'full')
        if history_mode == 'short':
            findings_history = findings_history.filter(pk__in=[])
            sections_history = sections_history.filter(pk__in=[])
            notes_history = notes_history.filter(pk__in=[])
        elif history_mode == 'medium':
            findings_history = findings_history.filter(Q(history_type__in=['+', '-']) | Q(history_change_reason__isnull=False))
            sections_history = sections_history.filter(Q(history_type__in=['+', '-']) | Q(history_change_reason__isnull=False))
            notes_history = notes_history.filter(Q(history_type__in=['+', '-']) | Q(history_change_reason__isnull=False))
        else:
            res += [
                UploadedImage.history.filter(linked_object_id=p.id).annotate(history_model_order=Value('06')),
                UploadedProjectFile.history.filter(linked_object_id=p.id).annotate(history_model_order=Value('07')),
            ]
        return res + [
            p.history.all().annotate(history_model_order=Value('01')),
            ProjectMemberInfo.history.filter(project_id=p.id).annotate(history_model_order=Value('05')),
            findings_history,
            sections_history,
            notes_history,
        ]


class PentestFindingViewSet(ProjectSubresourceMixin, HistoryTimelineViewSetMixin, viewsets.ModelViewSet):
    serializer_class = PentestFindingSerializer
    lookup_field = 'finding_id'
    lookup_url_kwarg = 'id'

    def get_serializer_class(self):
        if self.action == 'fromtemplate':
            return PentestFindingFromTemplateSerializer
        elif self.action == 'sort':
            return PentestFindingSortListSerializer
        return super().get_serializer_class()

    def get_queryset(self):
        return self.get_project().findings \
            .select_related('project__project_type', 'assignee')

    @action(detail=False, methods=['post'])
    def fromtemplate(self, request, *args, **kwargs):
        return super().create(request, *args, **kwargs)

    @action(detail=False, methods=['post'])
    @transaction.atomic
    def sort(self, request, *arg, **kwargs):
        serializer = self.get_serializer(instance=list(self.get_queryset()), data=request.data)
        serializer.is_valid(raise_exception=True)
        serializer.save()
        return Response(data=serializer.data)

    def get_history_timeline_queryset_parts(self):
        return [
            PentestFinding.history \
                .filter(project_id=self.get_project().id) \
                .filter(finding_id=self.kwargs['id']) \
                .annotate(model_id=F('finding_id')),
        ]


class ReportSectionViewSet(ProjectSubresourceMixin, HistoryTimelineViewSetMixin, mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet):
    serializer_class = ReportSectionSerializer
    lookup_field = 'section_id'
    lookup_url_kwarg = 'id'

    def get_queryset(self):
        return self.get_project().sections \
            .select_related('project__project_type', 'assignee')

    def get_history_timeline_queryset_parts(self):
        return [
            ReportSection.history \
                .filter(project_id=self.get_project().id) \
                .filter(section_id=self.kwargs['id']) \
                .annotate(model_id=F('section_id')),
        ]


class CommentViewSet(ProjectSubresourceMixin, viewsets.ModelViewSet):
    permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES + [CommentPermissions, license.ProfessionalLicenseRequired]

    def get_queryset(self):
        return Comment.objects \
            .filter_project(self.get_project()) \
            .select_related('user') \
            .prefetch_related(Prefetch('answers', queryset=CommentAnswer.objects.select_related('user')))

    def get_serializer_class(self, *args, **kwargs):
        if self.action == 'create':
            return CommentCreateSerializer
        elif self.action == 'resolve':
            return CommentResolveSerializer
        return CommentSerializer

    @action(detail=True, methods=['post'])
    def resolve(self, request, *args, **kwargs):
        return self.partial_update(request, *args, **kwargs)

    @transaction.atomic
    def create(self, request, *args, **kwargs):
        return super().create(request, *args, **kwargs)


@extend_schema(parameters=[OpenApiParameter(name='comment_id', type=UUID, location=OpenApiParameter.PATH)])
class CommentAnswerViewSet(ProjectSubresourceMixin, viewsets.ModelViewSet):
    permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES + [CommentPermissions, license.ProfessionalLicenseRequired]
    serializer_class = CommentAnswerSerializer

    @functools.cached_property
    def _get_comment(self):
        project = self.get_project()
        qs = Comment.objects \
            .filter_project(project) \
            .select_related('user')

        return get_object_or_404(qs, pk=self.kwargs['comment_pk'])

    def get_comment(self):
        return self._get_comment

    def get_queryset(self):
        return self.get_comment().answers \
            .select_related('user', 'comment')

    def get_serializer_context(self, *args, **kwargs):
        return super().get_serializer_context(*args, **kwargs) | {
            'comment': self.get_comment(),
        }


class PentestProjectHistoryViewSet(ProjectSubresourceMixin, HistoryViewSetBase):
    permission_classes = ProjectSubresourceMixin.permission_classes + [license.ProfessionalLicenseRequired]
    serializer_class = PentestProjectDetailSerializer

    def get_serializer_class(self):
        if self.action == 'finding':
            return PentestFindingSerializer
        elif self.action == 'section':
            return ReportSectionSerializer
        elif self.action == 'note':
            return ProjectNotebookPageSerializer
        elif self.action == 'note_excalidraw':
            return NoteExcalidrawDataSerializer
        return super().get_serializer_class()

    def get_serializer_context(self):
        historic_project = self.get_project().history.as_of(self.get_history_date())
        prefetch_related_objects([historic_project], Prefetch('project_type', ProjectType.history.as_of(self.get_history_date())))

        return super().get_serializer_context() | {
            'project': historic_project,
        }

    def get_base_object(self):
        return self.get_project()

    def retrieve(self, request, *args, **kwargs):
        return self.retrieve_historic(prefetch=[
            Prefetch('project_type', ProjectType.history.as_of(self.get_history_date())),
            # Skip members referencing deleted users
            Prefetch('members', ProjectMemberInfo.history.as_of(self.get_history_date()).filter(user__created__isnull=False).select_related('user')),
            Prefetch('sections', ReportSection.history.as_of(self.get_history_date()).select_related('assignee')),
            Prefetch('findings', PentestFinding.history.as_of(self.get_history_date()).select_related('assignee')),
        ])

    @extend_schema(parameters=[OpenApiParameter(name='id', type=UUID, location=OpenApiParameter.PATH)])
    @action(detail=True, url_path=f'findings/(?P<id>{UUIDConverter.regex})')
    def finding(self, request, *args, **kwargs):
        project = self.get_object()
        return self.retrieve_historic(
            get_instance=lambda: PentestFinding.history
                .as_of(self.get_history_date())
                .filter(project_id=project.id)
                .filter(finding_id=self.kwargs['id'])
                .select_related('assignee')
                .get(),
            prefetch=[
                Prefetch('project', PentestProject.history.as_of(self.get_history_date())),
                Prefetch('project__project_type', ProjectType.history.as_of(self.get_history_date())),
            ],
        )

    @extend_schema(parameters=[OpenApiParameter(name='id', type=UUID, location=OpenApiParameter.PATH)])
    @action(detail=True, url_path=f'sections/(?P<id>{StringConverter.regex})')
    def section(self, request, *args, **kwargs):
        return self.retrieve_historic(
            get_instance=lambda: ReportSection.history
                .as_of(self.get_history_date())
                .filter(project_id=self.get_object().id)
                .filter(section_id=self.kwargs['id'])
                .select_related('assignee')
                .get(),
            prefetch=[
                Prefetch('project', PentestProject.history.as_of(self.get_history_date())),
                Prefetch('project__project_type', ProjectType.history.as_of(self.get_history_date())),
            ],
        )

    @extend_schema(parameters=[OpenApiParameter(name='id', type=UUID, location=OpenApiParameter.PATH)])
    @action(detail=True, url_path=f'notes/(?P<id>{UUIDConverter.regex})')
    def note(self, request, *args, **kwargs):
        return self.retrieve_historic(
            get_instance=lambda: ProjectNotebookPage.history
                .as_of(self.get_history_date())
                .filter(project_id=self.get_object().id)
                .filter(note_id=self.kwargs['id'])
                .get(),
            prefetch=[
                Prefetch('parent', ProjectNotebookPage.history.as_of(self.get_history_date())),
            ],
        )

    @extend_schema(parameters=[OpenApiParameter(name='id', type=UUID, location=OpenApiParameter.PATH)])
    @action(detail=True, url_path=f'notes/(?P<id>{UUIDConverter.regex})/excalidraw/')
    def note_excalidraw(self, request, *args, **kwargs):
        return self.retrieve_historic(
            get_instance=lambda: ProjectNotebookPage.history
                .as_of(self.get_history_date())
                .filter(project_id=self.get_object().id)
                .filter(note_id=self.kwargs['id'])
                .get(),
            prefetch=[
                Prefetch('excalidraw_file', ProjectNotebookExcalidrawFile.history.as_of(self.get_history_date())),
            ],
        )

    @extend_schema(responses={(200, 'application/octet-stream'): OpenApiTypes.BINARY})
    @action(detail=True, url_path='images/name/(?P<filename>[^/]+)')
    @method_decorator(cache_control(max_age=60 * 60 * 24, private=True))
    def image_by_name(self, request, *args, **kwargs):
        return self.retrieve_historic_file(UploadedImage, filename=self.kwargs['filename'])

    @extend_schema(responses={(200, 'application/octet-stream'): OpenApiTypes.BINARY})
    @action(detail=True, url_path='files/name/(?P<filename>[^/]+)')
    @method_decorator(cache_control(max_age=60 * 60 * 24, private=True))
    def file_by_name(self, request, *args, **kwargs):
        return self.retrieve_historic_file(UploadedProjectFile, filename=self.kwargs['filename'])


@extend_schema(parameters=[OpenApiParameter(name='id', type=UUID, location=OpenApiParameter.PATH)])
class ArchivedProjectViewSet(TagViewSetMixin, mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
    permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES + [license.ProfessionalLicenseRequired]
    serializer_class = ArchivedProjectSerializer
    filter_backends = [SearchFilter, DjangoFilterBackend]
    search_fields = ['name', 'tags', 'key_parts__user__username', 'key_parts__user__email']
    filterset_class = ArchivedProjectFilterSet

    def get_queryset(self):
        return ArchivedProject.objects \
            .only_permitted(self.request.user) \
            .prefetch_related(Prefetch('key_parts', ArchivedProjectKeyPart.objects.select_related('user')))


@extend_schema(parameters=[OpenApiParameter(name='archivedproject_id', type=UUID, location=OpenApiParameter.PATH)])
@extend_schema(parameters=[OpenApiParameter(name='id', type=UUID, location=OpenApiParameter.PATH)])
class ArchivedProjectKeyPartViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
    permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES + [ArchivedProjectKeyPartPermissions, license.ProfessionalLicenseRequired]
    serializer_class = ArchivedProjectKeyPartSerializer
    pagination_class = None

    @functools.cached_property
    def _get_archived_project(self):
        qs = ArchivedProject.objects \
            .only_permitted(self.request.user)
        return get_object_or_404(qs, pk=self.kwargs['archivedproject_pk'])

    def get_archived_project(self):
        return self._get_archived_project

    def get_queryset(self):
        return self.get_archived_project().key_parts \
            .select_related('user')

    def get_serializer_class(self):
        if self.action == 'public_key_encrypted_data':
            return ArchivedProjectPublicKeyEncryptedKeyPartSerializer
        elif self.action == 'decrypt':
            return ArchivedProjectKeyPartDecryptSerializer
        return super().get_serializer_class()

    def get_serializer_context(self):
        return super().get_serializer_context() | {
            'archived_project': self.get_archived_project(),
        }

    @action(detail=True, url_path='public-key-encrypted-data', methods=['get'])
    def public_key_encrypted_data(self, request, *args, **kwargs):
        qs = self.get_object().public_key_encrypted_parts \
            .select_related('public_key')
        serializer = self.get_serializer(instance=qs, many=True)
        return Response(serializer.data)

    @action(detail=True, methods=['post'])
    def decrypt(self, request, *args, **kwargs):
        instance = self.get_object()
        serializer = self.get_serializer(instance=instance, data=request.data)
        serializer.is_valid(raise_exception=True)
        data = serializer.save()
        return Response(data)


class NotebookPageViewSetBaseMixin:
    pagination_class = None
    lookup_field = 'note_id'
    lookup_url_kwarg = 'id'
    create_serializer_class = None
    sort_serializer_class = None

    def get_serializer_class(self):
        if self.action == 'sort':
            return self.sort_serializer_class
        elif self.action == 'create':
            return self.create_serializer_class
        elif self.action == 'export_pdf':
            return ExportPdfOptionsSerializer
        elif self.action == 'export_pdf_multiple':
            return ExportPdfMultipleOptionsSerializer
        elif self.action == 'export_all':
            return ExportNotesOptionsSerializer
        elif self.action == 'excalidraw':
            return NoteExcalidrawDataSerializer
        return super().get_serializer_class()


class NotebookPageViewSetBase(NotebookPageViewSetBaseMixin, CopyViewSetMixin, ExportImportViewSetMixin, viewsets.ModelViewSet, ViewSetAsync):
    @action(detail=False, methods=['post'])
    @transaction.atomic
    def sort(self, request, *arg, **kwargs):
        serializer = self.get_serializer(instance=list(self.get_queryset()), data=request.data)
        serializer.is_valid(raise_exception=True)
        serializer.save()
        return Response(data=serializer.data)

    @extend_schema(responses={(200, 'application/pdf'): OpenApiTypes.BINARY, 401: PdfResponseSerializer})
    @action(detail=True, url_path='export-pdf', methods=['post'], throttle_scope='pdf')
    async def export_pdf(self, request, *args, **kwargs):
        instance = await self.aget_object()
        data = await render_note_to_pdf(notes=[instance], request=request)
        if data.pdf:
            return FileResponseAsync(BytesIO(data.pdf), content_type='application/pdf')
        else:
            return Response(data=data.to_dict(), status=status.HTTP_400_BAD_REQUEST)

    @extend_schema(responses={(200, 'application/pdf'): OpenApiTypes.BINARY, 401: PdfResponseSerializer})
    @action(detail=False, url_path='export-pdf', methods=['post'])
    async def export_pdf_multiple(self, request, *args, **kwargs):
        serializer = await self.aget_valid_serializer(data=request.data)
        data = await render_note_to_pdf(notes=serializer.validated_data['notes'], request=request)
        if data.pdf:
            return FileResponseAsync(BytesIO(data.pdf), content_type='application/pdf')
        else:
            return Response(data=data.to_dict(), status=status.HTTP_400_BAD_REQUEST)

    @extend_schema(responses={(200, 'application/octet-stream'): OpenApiTypes.BINARY})
    @action(detail=False, url_path='export', methods=['post'])
    def export_all(self, request, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)

        archive = self.perform_export(instances=serializer.validated_data.get('notes'))
        return StreamingHttpResponseAsync(streaming_content=archive, headers={
            'Content-Type': 'application/octet-stream',
            'Content-Disposition': 'inline',
        })

    @action(detail=True, methods=['get'])
    def excalidraw(self, request, **kwargs):
        return self.retrieve(request=request, **kwargs)


class ProjectNotebookPageViewSet(ProjectSubresourceMixin, HistoryTimelineViewSetMixin, NotebookPageViewSetBase):
    serializer_class = ProjectNotebookPageSerializer
    create_serializer_class = ProjectNotebookPageCreateSerializer
    sort_serializer_class = ProjectNotebookPageSortListSerializer

    def get_queryset(self):
        qs = self.get_project().notes.all() \
            .annotate_is_shared() \
            .select_related('parent') \
            .order_by('parent', 'order')
        if self.action == 'export_pdf':
            qs = qs.select_related('project')
        return qs

    def get_history_timeline_queryset_parts(self):
        return [
            ProjectNotebookPage.history \
                .filter(project_id=self.get_project().id) \
                .filter(note_id=self.kwargs['id']) \
                .annotate(model_id=F('note_id')),
        ]

    def perform_export(self, instances=None):
        return export_notes(self.get_project(), notes=instances)

    def perform_import(self, archive, data, **kwargs):
        return import_notes(archive, context={'project': self.get_project()})


class UserNotebookPageViewSet(UserSubresourceViewSetMixin, NotebookPageViewSetBase):
    permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES + [UserNotebookPermissions]
    serializer_class = UserNotebookPageSerializer
    create_serializer_class = UserNotebookPageCreateSerializer
    sort_serializer_class = UserNotebookPageSortListSerializer

    def get_queryset(self):
        qs = self.get_user().notes.all() \
            .select_related('parent') \
            .order_by('parent', 'order')
        if self.action == 'export_pdf':
            qs = qs.select_related('user')
        return qs

    def get_serializer_class(self):
        if self.action == 'upload_image_or_file':
            return UploadedUserNotebookFileSerilaizer
        return super().get_serializer_class()

    @action(detail=False, url_path='upload', methods=['post'])
    def upload_image_or_file(self, request, *args, **kwargs):
        # First try saving an image, then saving as a regular file
        serializer_context = self.get_serializer_context() | {'user': self.get_user()}
        serializer = UploadedUserNotebookImageSerializer(data=request.data, context=serializer_context)
        if not serializer.is_valid(raise_exception=False):
            serializer = UploadedUserNotebookFileSerilaizer(data=request.data, context=serializer_context)
            serializer.is_valid(raise_exception=True)

        serializer.save()
        return Response(data=serializer.data, status=status.HTTP_201_CREATED)

    def perform_export(self, instances=None):
        return export_notes(self.get_user(), notes=instances)

    def perform_import(self, archive, data, **kwargs):
        return import_notes(archive, context={'user': self.get_user()})


@csp({'default-src': [NONE]})
def file_response(instance):
    content_type = 'application/octet-stream'
    as_attachment = True
    if isinstance(instance.file.field, ImageField):
        guessed_content_type, _ = mimetypes.guess_file_type(instance.name)
        if guessed_content_type and guessed_content_type in [
            'image/png', 'image/jpeg', 'image/gif', 'image/tiff', 'image/bmp', 'image/webp',
        ]:
            content_type = guessed_content_type
            as_attachment = False
    return FileResponseAsync(
        instance.file.open(),
        filename=instance.name,
        content_type=content_type,
        as_attachment=as_attachment,
    )


class UploadedFileViewSetMixin:
    @extend_schema(responses={(200, 'application/octet-stream'): OpenApiTypes.BINARY})
    @action(detail=False, url_path='name/(?P<filename>[^/]+)')
    @method_decorator(cache_control(max_age=60 * 60 * 24, private=True))
    def retrieve_by_name(self, request, *args, **kwargs):
        queryset = self.filter_queryset(self.get_queryset())
        instance = get_object_or_404(queryset.filter_name(kwargs['filename']))
        self.check_object_permissions(request, instance)
        return file_response(instance)


class UploadedImageViewSet(ProjectSubresourceMixin, UploadedFileViewSetMixin, viewsets.ModelViewSet):
    serializer_class = UploadedImageSerializer
    pagination_class = api_settings.DEFAULT_PAGINATION_CLASS

    def get_queryset(self):
        return self.get_project().images.all()


class UploadedTemplateImageViewSet(TemplateSubresourceMixin, UploadedFileViewSetMixin, viewsets.ModelViewSet):
    serializer_class = UploadedTemplateImageSerializer
    pagination_class = api_settings.DEFAULT_PAGINATION_CLASS

    def get_queryset(self):
        return self.get_template().images.all()


class UploadedProjectFileViewSet(UploadedImageViewSet):
    serializer_class = UploadedProjectFileSerilaizer

    def get_queryset(self):
        return self.get_project().files.all()


class UploadedUserNotebookImageViewSet(UserSubresourceViewSetMixin, UploadedFileViewSetMixin, viewsets.ModelViewSet):
    serializer_class = UploadedUserNotebookImageSerializer
    permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES + [UserNotebookPermissions]
    pagination_class = api_settings.DEFAULT_PAGINATION_CLASS

    def get_queryset(self):
        return self.get_user().images.all()


class UploadedUserNotebookFileViewSet(UploadedUserNotebookImageViewSet):
    serializer_class = UploadedUserNotebookFileSerilaizer

    def get_queryset(self):
        return self.get_user().files.all()


class UploadedAssetViewSet(ProjectTypeSubresourceMixin, UploadedFileViewSetMixin, viewsets.ModelViewSet):
    permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES + [ProjectTypeSubresourcePermissions]
    serializer_class = UploadedAssetSerializer

    def get_queryset(self):
        return self.get_project_type().assets.all()


@extend_schema(parameters=[OpenApiParameter(name='id', type=UUID, location=OpenApiParameter.PATH)])
class FindingTemplateViewSet(LockableViewSetMixin, CopyViewSetMixin, TagViewSetMixin, ExportImportViewSetMixin, viewsets.ModelViewSet):
    permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES + [IsTemplateEditorOrReadOnly]
    serializer_class = FindingTemplateSerializer
    filter_backends = [DjangoFilterBackend, FindingTemplateSearchFilter, FindingTemplateOrderingFilter]
    filterset_class = FindingTemplateFilter
    pagination_class = CursorMultiPagination

    def get_queryset(self):
        return FindingTemplate.objects \
            .select_related('main_translation', 'lock_info_data', 'lock_info_data__user') \
            .prefetch_related(Prefetch('translations', FindingTemplateTranslation.objects.default_order()))

    def get_serializer_class(self):
        if self.action == 'create':
            return FindingTemplateSerializer
        elif self.action == 'list':
            return FindingTemplateShortSerializer
        elif self.action == 'fromfinding':
            return FindingTemplateFromPentestFindingSerializer
        return super().get_serializer_class()

    @extend_schema(responses=OpenApiTypes.OBJECT)
    @action(detail=False)
    def fielddefinition(self, request, *args, **kwargs):
        return Response(data=serialize_field_definition(FindingTemplate.field_definition, extra_info=True))

    @action(detail=False, methods=['post'])
    def fromfinding(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)

    def perform_export(self, instances):
        return export_templates(instances)

    def perform_import(self, archive, data, **kwargs):
        return import_templates(archive, **kwargs)


class FindingTemplateTranslationViewSet(TemplateSubresourceMixin, HistoryTimelineViewSetMixin, viewsets.ModelViewSet):
    serializer_class = FindingTemplateTranslationSerializer

    def get_queryset(self):
        return self.get_template().translations.all().default_order()

    def perform_destroy(self, instance):
        if instance.is_main:
            raise ValidationError('Cannot delete main template language')
        return super().perform_destroy(instance)

    def get_history_timeline_queryset_parts(self):
        return [
            FindingTemplateTranslation.history.filter(template_id=self.get_template().id).filter(id=self.kwargs['pk']),
        ] + ([
            self.get_template().history.all(),
            FindingTemplateTranslation.history.filter(template_id=self.get_template().id).filter(history_type__in=['+', '-']),
        ] if self.request.GET.get('include_template_timeline') else [])


class FindingTemplateHistoryViewSet(TemplateSubresourceMixin, HistoryViewSetBase):
    serializer_class = FindingTemplateSerializer
    permission_classes = TemplateSubresourceMixin.permission_classes + [license.ProfessionalLicenseRequired]

    def get_base_object(self):
        return self.get_template()

    def retrieve(self, request, *args, **kwargs):
        return self.retrieve_historic(prefetch=[
            Prefetch('translations', FindingTemplateTranslation.history \
                .as_of(self.get_history_date())
                .annotate(is_main_order=Q(id=F('template__main_translation_id')))
                .order_by('-is_main_order', 'created')),
        ])

    @extend_schema(responses={(200, 'application/octet-stream'): OpenApiTypes.BINARY})
    @action(detail=True, url_path='images/name/(?P<filename>[^/]+)')
    @method_decorator(cache_control(max_age=60 * 60 * 24, private=True))
    def image_by_name(self, request, *arg, **kwargs):
        return self.retrieve_historic_file(UploadedTemplateImage, filename=self.kwargs['filename'])


class UserPublicKeyViewSet(UserSubresourceViewSetMixin, mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, viewsets.GenericViewSet):
    serializer_class = UserPublicKeySerializer
    permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES + [UserPublicKeyPermissions, license.ProfessionalLicenseRequired]

    def get_queryset(self):
        return self.get_user().public_keys.all()

    def get_serializer_class(self):
        if self.action == 'register_begin':
            return UserPublicKeyRegisterBeginSerializer
        return super().get_serializer_class()

    @action(detail=False, url_path='register/begin', methods=['post'])
    def register_begin(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        instance = serializer.save()

        # Verify that users can decrypt with the given public key before registering
        test_data = 'key-verification-' + str(uuid4()) + '\n'
        verification_encrypted = instance.encrypt(test_data.encode())
        self.request.session['public_key_register'] = {
            'instance': model_to_dict(instance),
            'verification': test_data,
        }
        return Response(data={
            'status': 'verify-key',
            'public_key_info': instance.public_key_info,
            'verification': verification_encrypted,
        })

    @action(detail=False, url_path='register/complete', methods=['post'])
    def register_complete(self, request, *args, **kwargs):
        public_key_register_state = request.session.get('public_key_register')
        if not public_key_register_state:
            raise APIBadRequestError('No public key registration in progress')
        if public_key_register_state['verification'].strip() != request.data.get('verification', '').strip():
            raise ValidationError('Invalid verification code')

        instance = UserPublicKey(**public_key_register_state['instance'])
        instance.user = self.get_user()
        instance.save()
        serializer = self.get_serializer(instance=instance)
        return Response(data=serializer.data, status=status.HTTP_201_CREATED)

    def perform_destroy(self, instance):
        try:
            instance.delete()
        except ProtectedError as ex:
            raise ValidationError(
                detail='Cannot delete this public key because some archives are encrypted with it. '
                       'You can disable it to not be used for archiving in the future.',
            ) from ex


@extend_schema(parameters=[OpenApiParameter(name='note_id', type=UUID, location=OpenApiParameter.PATH)])
class ShareInfoViewSet(ProjectSubresourceMixin, mixins.CreateModelMixin, mixins.UpdateModelMixin, viewsets.ReadOnlyModelViewSet):
    permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES + [ShareInfoPermissions]
    serializer_class = ShareInfoSerializer

    @functools.cached_property
    def _get_note(self):
        if not self.request:
            return None
        qs = self.get_project().notes.all()
        return get_object_or_404(qs, note_id=self.kwargs.get('note_id'))

    def get_note(self):
        return self._get_note

    def get_queryset(self):
        return self.get_note().shareinfos.all()

    def get_serializer_context(self):
        return super().get_serializer_context() | {
            'note': self.get_note(),
        }


class ShareInfoPublicViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
    authentication_classes = []
    permission_classes = [ShareInfoPublicPermissions]

    def get_queryset(self):
        return ShareInfo.objects \
            .only_active() \
            .select_related('note__project')

    def get_serializer_class(self):
        if self.action == 'auth':
            return ShareInfoCheckPasswordSerializer
        return ShareInfoPublicSerializer

    @action(detail=True, methods=['post'])
    def auth(self, request, *args, **kwargs):
        instance = self.get_object()
        serializer = self.get_serializer(instance=instance, data=request.data)
        serializer.is_valid(raise_exception=True)

        request.session.setdefault('authorized_shareids', [])
        if str(instance.id) not in request.session['authorized_shareids']:
            request.session['authorized_shareids'].append(str(instance.id))
        return Response(data={'status': 'ok'})


@extend_schema(parameters=[OpenApiParameter(name='shareinfo_id', type=UUID, location=OpenApiParameter.PATH)])
@extend_schema(parameters=[OpenApiParameter(name='id', type=UUID, location=OpenApiParameter.PATH)])
class SharedProjectNotePublicViewSet(NotebookPageViewSetBaseMixin, viewsets.ModelViewSet):
    authentication_classes = []
    permission_classes = [SharedProjectNotePublicPermissions]
    serializer_class = ProjectNotebookPagePublicSerializer
    create_serializer_class = ProjectNotebookPageCreatePublicSerializer

    @functools.cached_property
    def _get_share_info(self):
        qs = ShareInfo.objects \
            .only_active() \
            .select_related('note__project')
        return get_object_or_404(qs, pk=self.kwargs['shareinfo_pk'])

    def get_share_info(self):
        return self._get_share_info

    def get_serializer_class(self):
        if self.action == 'upload_image_or_file':
            return UploadedProjectFileSerilaizer
        return super().get_serializer_class()

    def get_serializer_context(self):
        return super().get_serializer_context() | {
            'share_info': self.get_share_info(),
        }

    def get_queryset(self):
        return ProjectNotebookPage.objects \
            .child_notes_of(self.get_share_info().note)

    def perform_destroy(self, instance):
        if instance == self.get_share_info().note:
            raise ValidationError('Cannot delete the main note')
        return super().perform_destroy(instance)

    @action(detail=False, url_path='upload', methods=['post'])
    def upload_image_or_file(self, request, *args, **kwargs):
        # First try saving an image, then saving as a regular file
        serializer_context = self.get_serializer_context() | {'project': self.get_share_info().note.project}
        serializer = UploadedImageSerializer(data=request.data, context=serializer_context)
        if not serializer.is_valid(raise_exception=False):
            serializer = UploadedProjectFileSerilaizer(data=request.data, context=serializer_context)
            serializer.is_valid(raise_exception=True)

        serializer.save()
        return Response(data=serializer.data, status=status.HTTP_201_CREATED)

    def _retrieve_by_name(self, queryset, **kwargs):
        instance = get_object_or_404(queryset.filter_name(kwargs['filename']))
        shared_notes = ProjectNotebookPage.objects.child_notes_of(self.get_share_info().note)
        if not any(n.is_file_referenced(instance) for n in shared_notes) or instance.created >= timezone.now() + timedelta(minutes=1):
            # Allow only accessing files that are referenced in shared notes (or child notes).
            # Newly created files might not be referenced yet, because the reference was not saved in the DB yet.
            raise exceptions.PermissionDenied('File not referenced in any shared note')
        return file_response(instance)

    @extend_schema(responses={(200, 'application/octet-stream'): OpenApiTypes.BINARY})
    @action(detail=False, url_path='images/name/(?P<filename>[^/]+)')
    @method_decorator(cache_control(max_age=60 * 60 * 24, private=True))
    def image_by_name(self, request, *args, **kwargs):
        return self._retrieve_by_name(queryset=self.get_share_info().note.project.images.all(), **kwargs)

    @extend_schema(responses={(200, 'application/octet-stream'): OpenApiTypes.BINARY})
    @action(detail=False, url_path='files/name/(?P<filename>[^/]+)')
    @method_decorator(cache_control(max_age=60 * 60 * 24, private=True))
    def file_by_name(self, request, *args, **kwargs):
        return self._retrieve_by_name(queryset=self.get_share_info().note.project.files.all(), **kwargs)

    @action(detail=True, methods=['get'])
    def excalidraw(self, request, **kwargs):
        return self.retrieve(request=request, **kwargs)

